package com.ap.core.util;

/**
 * Tiny type used to represent values that may or may not be null. Still throws a null pointer exception if someone
 * attempts to use the null value but throws it in context of the nullable which should make it easier to trace the
 * route cause.
 * <p>
 * Equals and Hashcode are properly implemented but all Nullables representing null values are the same instance.
 * Therefore adding multiple null represented values to Sets will result in only one entry. It is also not a good idea
 * to use Nullables as keys in maps as the null representation will produce only one entry.
 * <p>
 * The thinking is that you wouldn't expect to differentiate 'null's in a set or use them as keys in a map...
 */
public class Nullable<T> {

    private final T nullableValue;

    /**
     * Singleton instance that is used to represent all 'Null' values. Allows for direct checking as well as indirect
     * checking of null status.
     */
    public static final Nullable<Object> NULL = new Nullable<>(null);

    private Nullable(T nullableValue) {
        this.nullableValue = nullableValue;
    }

    /**
     * Method used to determine whether this represents a null value.
     * @return true if the value represented is null.
     */
    public boolean hasNull(){
        return nullableValue == null;
    }


    /**
     * Method used to determine whether this represents a not-null value.
     * @return true if the value represented is not null.
     */
    public boolean hasValue() {
        return nullableValue != null;
    }

    /**
     * Retrieve the value that is being represented.
     * @return the value that is being represented.
     * @throws NullPointerException if the value is null - intended to improve traceability.
     */
    public T value(){
        if (hasValue()) {
            return nullableValue;
        }
        throw new NullPointerException("Attempting to get a null value from this nullable.");
    }

    @Override
    public String toString() {
        return "Nullable{" +
                "nullableValue=" + nullableValue +
                '}';
    }

    /**
     * Method used to instantiate a Nullable instance.
     * @param value The value that the Nullable represents.
     * @param <T> The type of the value being represented.
     * @return a Nullable representing the value.
     */
    @SuppressWarnings("unchecked")
    public static <T> Nullable<T> nullable(T value){
        if(value != null) {
            return new Nullable<T>(value);
        }
        return (Nullable<T>) NULL;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Nullable)) return false;

        Nullable nullable = (Nullable) o;

        if (!nullableValue.equals(nullable.nullableValue)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return nullableValue.hashCode();
    }
}
